package sicnu.cs.ich.ich.service.impl;

import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import io.jsonwebtoken.lang.Collections;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.remoting.exception.RemotingException;
import org.springframework.beans.BeanUtils;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.DefaultTypedTuple;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.web.multipart.MultipartFile;
import sicnu.cs.ich.api.common.Constants;
import sicnu.cs.ich.api.common.enums.FileEnum;
import sicnu.cs.ich.api.domain.dto.*;
import sicnu.cs.ich.api.domain.form.IchForm;
import sicnu.cs.ich.api.domain.form.IchQueryParam;
import sicnu.cs.ich.api.domain.form.PageParam;
import sicnu.cs.ich.api.domain.entity.Ich;
import sicnu.cs.ich.api.domain.entity.IchProvince;
import sicnu.cs.ich.api.domain.vo.AdminIchVo;
import sicnu.cs.ich.common.services.IFileUploadService;
import sicnu.cs.ich.common.services.impl.RedisService;
import sicnu.cs.ich.ich.mapper.IchMapper;
import sicnu.cs.ich.ich.mapper.IchTypeMapper;
import sicnu.cs.ich.ich.service.IIchService;
import sicnu.cs.ich.token.security.util.SecurityUtil;

import java.io.FileInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

import static sicnu.cs.ich.api.common.Constants.EXPIRE_ICH_TIME;


/**
 * @author Caike
 */
@Service
@Slf4j
@RequiredArgsConstructor
@CacheConfig(cacheNames = "ich")
public class IchServiceImpl implements IIchService {
    private final RedisTemplate<String,Object> redisTemplate;
    private final IchMapper ichMapper;
    private final IchTypeMapper ichTypeMapper;
    private final RedisService redisService;
    private final GuavaCacheService guavaCacheService;
    private final IFileUploadService fileUploadService;
    private static final String MUTEX_KEY = "MUTEX_";
    /**
     * Redis缓存非遗的前缀
     */
    private static final String REDIS_ICH = Constants.REDIS_ICH;
    private final SecurityUtil securityUtil;


    @Override
    @Cacheable(key = "methodName", sync = true)
    public List<IchProvinceDTO> getProvinceIch() {
        List<IchProvinceDTO> dtos = ichMapper.listIchByProvince();
        if (dtos.isEmpty()) {
            throw new NoSuchElementException("Cannot find Data!");
        }
        return dtos;
    }

    /**
     * 获取各省的非遗数据（结果按照非遗数量降序排列）
     *
     * @return 市id、市名字、市非遗数量的列表
     */
    @Override
    public List<IchCityDTO> getCityIch(Integer province) {
        List<IchCityDTO> dtos = ichMapper.listIchByCity(province);
        if (dtos.isEmpty()) {
            throw new NoSuchElementException("Cannot find Data!");
        }
        return dtos;
    }

    /**
     * 获取单个非遗信息
     *
     * @param id 非遗id
     * @return 非遗
     */
    @Override
    public Ich getIchById(Integer id) throws InterruptedException {
        Objects.requireNonNull(id);
        // 这里的Cacheable优先级高于IchHitAOP，
        //  导致无法正常统计非遗热度（明明指定了Ich热度统计的优先度为最高，Giao！）
        // 手动实现缓存（格式指定为 ich::getIchById::{id}，实现伪统一）（时间指定为 5min）
        Ich ich = null;
        String key = REDIS_ICH + id;
        ich = (Ich) redisService.getValue(key);
        //缓存失效
        if (ich == null) {
            //设置分布式锁，只允许一个线程去查询DB，同时指定过期时间为1min，防止del操作失败，导致死锁，缓存过期无法加载DB数据
            if (redisService.lock(MUTEX_KEY + key, 60L, TimeUnit.SECONDS)) {
                //从数据库查询数据,将查询的结果缓存起来
                ich = ichMapper.selectById(id);
                if (ich != null) {
                    redisService.cacheValue(key, ich, EXPIRE_ICH_TIME);
                }
                //释放分布式锁
                redisService.unlock(MUTEX_KEY + key);
            } else {
                log.info(Thread.currentThread().getName() + " 未抢到锁继续等待");
                //当锁被占用时，睡眠2s继续调用获取数据请求
                Thread.sleep(2000);
                return getIchById(id);
            }
        }
        return ich;
    }

    @Override
    public PageInfo<IchDTO> getIchsByTypeId(Integer typeId, PageParam param, Integer province) {
        if (ichTypeMapper.countByTypeId(typeId) < 1) {
            throw new IllegalArgumentException("无此TypeId对应的类别信息。");
        }
        PageHelper.startPage(param.getPageNum(), param.getPageSize());
        var list = ichMapper.selectIchsByType(typeId, province);
        return new PageInfo<>(list);
    }

    /**
     * 获取非遗单品集合
     *
     * @param ids id集合
     * @return 非遗列表
     */
    @Override
    public List<IchDTO> getIchByList(List<Integer> ids) {
        if (Collections.isEmpty(ids)) {
            throw new IllegalArgumentException("ids不能为空");
        }
        List<Integer> idsTo = new LinkedList<>();
        List<IchDTO> ichs = new LinkedList<>();
        String k = REDIS_ICH;
        ids.forEach(id -> {
            val key = k + id;
            if (redisService.containsKey(key)) {
                Ich ich = (Ich) redisService.getValue(key);
                IchDTO ichDTO = new IchDTO();
                BeanUtils.copyProperties(ich, ichDTO);
                ichs.add(ichDTO);
            } else {
                idsTo.add(id);
            }
        });
        if (!idsTo.isEmpty()) {
            final List<Ich> ichFromSql = ichMapper.selectByIds(idsTo);
            ichFromSql.forEach(ich -> {
                var key = k + ich.getId();
                redisService.cacheValue(key, ich, 20 * 60 + new Random().nextInt(10 * 60));
                IchDTO ichDTO = new IchDTO();
                BeanUtils.copyProperties(ich, ichDTO);
                ichs.add(ichDTO);
            });
        }
        return ichs;
    }

    @Override
    @CacheEvict(cacheNames = {"ich"}, allEntries = true)
    public void clearCache() {
        log.info("ich Service 清空缓存！");
    }

    @Override
    public IchProvince ichProvince(Integer province) {
        var ichProvince = (IchProvince) redisService.getValue(Constants.REDIS_ICH_PROVINCE_INFO + province);
        if (ichProvince == null) {
            ichProvince = ichMapper.selectIchProvince(province);
            if (ichProvince == null) {
                throw new NoSuchElementException("未找到对应省份信息");
            }
            redisService.cacheValue(Constants.REDIS_ICH_PROVINCE_INFO + province, ichProvince);
        }
        return ichProvince;
    }


    /**
     * 获取地区下的各类型非遗总数
     *
     * @param regionId 地区id
     */
    @Override
    public List<IchTypeDTO> getTypeCountOfRegion(Integer regionId) {
        return ichTypeMapper.countByRegionId(regionId);
    }

    @Override
    public Integer getIchLikeNumById(Integer ichId) {
        return ichMapper.selectIchLikingById(ichId);
    }

    @Override
    @CacheEvict(value = "ich::like::id", allEntries = true)
    public void clearLikeNum() {
        log.info("删除Redis点赞信息");
    }

    @Override
    public IchProvinceDTO getProvinceTypeHotIchs(Integer province) throws ExecutionException {
        // guava中取数据，guava中缓存10分钟
        var provinceDTO = guavaCacheService.getIchProvinceDTO(province);
        if (provinceDTO == null) {
            // redis中取
            provinceDTO = (IchProvinceDTO) redisService.getValue(Constants.REDIS_RANK_ICH_PROVINCE_TYPE + province);
            if (provinceDTO == null) {
                // 数据库中取
                provinceDTO = ichMapper.selectProvinceDTO(province);
                if (provinceDTO == null) {
                    throw new NoSuchElementException("未找到数据");
                }
                var typeList = provinceDTO.getTypeDTOS();
                for (IchTypeDTO ichTypeDTO : typeList) {
                    ichTypeDTO.setIchs(ichMapper.selectProvinceTypeHotIch(province, ichTypeDTO.getId()));
                }
                // 设置缓存过期时间在20分钟到30分钟之间，避免缓存雪崩
                redisService.cacheValue(Constants.REDIS_RANK_ICH_PROVINCE_TYPE + province
                        , provinceDTO, 1200 + new Random().nextInt(600));
            }
            // 将数据放入guava缓存中
            guavaCacheService.setIchProvinceDTO(province, provinceDTO);
        }
        return provinceDTO;
    }

    /**
     * 收场非遗
     */
    @Override
    public boolean collectIch(Integer ichId) {
        return ichMapper.insertFavoriteIch(securityUtil.getCurrentUserId(), ichId)>0;
    }

    /**
     * 获取收藏的非遗目录
     *
     * @param param 分页参数
     */
    @Override
    public PageInfo<IchDTO> getFavoriteIchs(PageParam param) {
        PageHelper.startPage(param.getPageNum(), param.getPageSize());
        List<IchDTO> list = ichMapper.selectFavorite(securityUtil.getCurrentUserId());
        return new PageInfo<>(list);
    }

    /**
     * 查询是否收藏
     *
     * @param ichId 非遗id
     */
    @Override
    public boolean isCollected(Integer ichId) {
        return ichMapper.countFavorite(securityUtil.getCurrentUserId(), ichId) > 0;
    }

    /**
     * 删除收藏非遗
     *
     * @param ichId 非遗Id
     */
    @Override
    public boolean deleteFavoriteIch(Integer ichId) {
        return ichMapper.deleteFavoriteIch(securityUtil.getCurrentUserId(), ichId) > 0;
    }

    /**
     * 获取相似非遗
     *
     * @param ichId 非遗id
     * @return 相似非遗列表
     */
    @Override
    public List<IchDTO> getSimilarIchsByType(Integer ichId, Integer typeId, Integer province) {
        return ichMapper.findTypeSimilarIchsById(ichId, typeId, province);
    }

    /**
     * 获取类型名
     *
     * @param typeId 类型id
     */
    @Override
    public String getType(Integer typeId) {
        return ichTypeMapper.selectType(typeId);
    }

    /**
     * 获取所有非遗数据id
     */
    @Override
    public List<Integer> getAllIchId() {
        return ichMapper.selectAllId();
    }

    @Override
    public List<IchHotDTO> getProvinceIchHotDTO(String province, Integer preNum) {
        if (preNum <=1  || preNum > 100) {
            throw new IllegalArgumentException("参数错误");
        }
        String key = Constants.REDIS_RANK_ICH_PROVINCE + province;
        var set = redisTemplate.opsForZSet().reverseRangeWithScores(key,0,preNum-1);
        List<IchHotDTO> list = null;
        if (set==null||set.size() < preNum) {
            // 如果redis中获取的排行榜个数小于需要的数量，则从数据库中找
            list = ichMapper.selectProvinceIchHotDTO(province, preNum);
            // 判断是否有数据
            if (CollectionUtils.isEmpty(list)) {
                throw new NoSuchElementException("无数据");
            }
            // 将数据库中的数据重新放入redis中
            var newRank = new HashSet<ZSetOperations.TypedTuple<Object>>();
            for (IchHotDTO hotDTO : list) {
                // 一定要置为null，否者无法同步数据
                hotDTO.setHot(0);
                newRank.add(new DefaultTypedTuple<>(hotDTO, (double) 0));
            }
            redisService.cacheZset(key, newRank);
        } else {
            list = new ArrayList<>(set.size());
            for (ZSetOperations.TypedTuple<Object> tuple : set) {
                var ichHotDTO = (IchHotDTO) tuple.getValue();
                if (ichHotDTO != null) {
                    ichHotDTO.setHot(Objects.requireNonNull(tuple.getScore()).intValue());
                    list.add(ichHotDTO);
                }
            }
        }
        return list;
    }

    @Override
    @Transactional(rollbackFor = RuntimeException.class)
    public void deleteIchById(Integer id) throws MQClientException, RemotingException, InterruptedException, MQBrokerException {
        if (ichMapper.countIchById(id) < 0) {
            throw new NoSuchElementException("无该id对应的非遗！");
        }
        ichMapper.deleteById(id);
        // 使用canal监听Mysql的binlog，删除日志
//        // 调用消息队列，删除缓存
//        String key = REDIS_ICH_PREFIX + id;
//        Message message = new Message("redis", "del", JSON.toJSONBytes(key));
//        producer.send(message);
    }

    @Override
    public void cacheIchs(List<Integer> ids, Long seconds) {
        List<Integer> ichs = new ArrayList<>(ids.size());
        ids.forEach(id -> {
            final var key = REDIS_ICH + id;
            if (redisService.containsKey(key)) {
                // 10分钟的波动
                redisService.setEx(key, seconds + new Random().nextInt(10 * 60));
            } else {
                ichs.add(id);
            }
        });
        final List<Ich> ichList = ichMapper.selectByIds(ichs);
        ichList.forEach(ich -> {
            String key = REDIS_ICH + ich.getId();
            // 10分钟的波动
            redisService.cacheValue(key, ich, seconds + new Random().nextInt(10 * 60));
        });
    }

    @Override
    public AdminIchVo getAdminIchVo(Integer ichId) {
        return ichMapper.findAdminIchVoById(ichId);
    }

    /**
     * 分页获取非遗列表
     */
    @Override
    public PageInfo<IchDTO> getIchDTOS(IchQueryParam param) {
        PageHelper.startPage(param.getPageNum(),param.getPageSize());
        var list = ichMapper.selectIchDTO(param);
        if (list==null)
            throw new NoSuchElementException("无数据");
        return new PageInfo<>(list);
    }

    /**
     * 判断非遗是否存在
     */
    @Override
    public boolean isIchExisted(Integer ichId) {
        return ichMapper.selectCountByIchId(ichId)>0;
    }

    /**
     * 更新非遗照片
     */
    @Override
    public boolean updateIchCoverImage(MultipartFile file, Integer ichId) throws IOException {
        var key = fileUploadService.uploadFile((FileInputStream) file.getInputStream(), FileEnum.ICH,file.getOriginalFilename());
        return ichMapper.updateById(Ich.builder()
                .id(ichId)
                .img(key)
                .build())>0;
    }

    /**
     * 新增非遗
     */
    @Override
    public boolean addIch(IchForm ichForm) {
        return ichMapper.insert(ichForm)>0;
    }

    /**
     * 非遗图片入库
     */
    @Override
    public boolean addIchImages(Integer ichId, List<String> keys) {
        return ichMapper.insertIchImage(ichId,keys)>0;
    }

}